'use strict';

const coreVersion = '0.1.1';

// NOTE
// must sure to include this before use:
// https://github.com/macmcmeans/localDataStorage

// localDataStorage
const LDSKey = 'figey';
const ls = localDataStorage(LDSKey);

// proxy console object for logs
const log = new Proxy(console, {
	get: function(target, property){
		const origMethod = target[property];
		return function(...args){
			if( args.every( function(s){
					return typeof s == 'string';
				})
			){
				args.unshift( GM_info.script.name + ': ');
			}

			let result = origMethod.apply(this, args);
			return result;
		};
	}
});

const HWMMap = {
	/**
	 * Trying to get map parameters for future work
	 */
	init(){
		/*
		 * check window.onload
		 * check localStorage
		 */
		const parseParams = function(p){
			return {
				sector: +p[0].split('*').pop(),
				sectorView: +p[1],
				sectorMG: +p[13],
				sectorMoveFrom: +p[14],
				move: +p[14] > 0 ? true : false,
				moveTimeLeft: +p[15],
				moveTimeTotal: +p[16],
			};
		}
		const timerKeyHG = 'timerHG';

		// force rewrite if we have new data
		if( document.body.innerHTML.match(/init_map\(.*\)/g) ){
			const initMap = document.body.innerHTML.match(/init_map\(.*\)/g);
			const mapParams = initMap[0].match(/\".*?\"/g)[2].split(':', 22);
			const map = parseParams(mapParams);
			ls.set('map', map);
			ls.set('mapTimestamp', Date.now());
			// check if hunt available
			if( mapParams[11].length > 0 ){
				ls.set(timerKeyHG, 0);
			} else if( document.body.innerHTML.match(/var Delta2 = (\d+)/) ){
				ls.set(timerKeyHG, Date.now() + 1000*document.body.innerHTML.match(/var Delta2 = (\d+)/)[1]);
			}
		// skip init if already have map info, except map page
		} else if( typeof ls.get('mapTimestamp') !== 'undefined' && Date.now() - ls.get('mapTimestamp') < ( ls.get('map').move ? 1000*ls.get('map').moveTimeLeft : 1000*60 ) ){
			return;
		// get map.php page with XMLHttpRequest and write result to localDataStorage
		} else {
			const xhr = new XMLHttpRequest();
			xhr.open('GET', 'map.php', false);
			xhr.overrideMimeType('text/html; charset=windows-1251');
			xhr.send(null);
			// work only with successful request
			if( xhr.status === 200 && xhr.readyState === 4 ){
				const initMap = xhr.responseText.match(/init_map\(.*\)/g);
				if( initMap && initMap.length === 1 ){
					const mapParams = initMap[0].match(/\".*?\"/g)[2].split(':', 22);
					const map = parseParams(mapParams);
					ls.set('map', map);
					ls.set('mapTimestamp', Date.now());
					// check if hunt available
					if( mapParams[11].length > 0 ){
						ls.set(timerKeyHG, 0);
					} else if( xhr.responseText.match(/var Delta2 = (\d+)/) ){
						ls.set(timerKeyHG, Date.now() + 1000*xhr.responseText.match(/var Delta2 = (\d+)/)[1] );
					}
				} else {
					log.error("No map data in XHR response");
				}
			} else {
				log.error("Cannot find map params!");
				// TODO need clear localDataStorage?
			}
		}
	},

	/**
	 * Array with sector's objects
	 */
	sectorList: [
		{ id: 23,	x: 48, y: 48, name: 'Ungovern. Steppe' },
		{ id: 13,	x: 48, y: 49, name: 'Sunny City' },
		{ id: 27,	x: 48, y: 50, name: 'Sublime Arbor' },

		{ id: 9,	x: 49, y: 48, name: 'Eagle Nest' },
		{ id: 12,	x: 49, y: 49, name: 'Shining Spring' },
		{ id: 8,	x: 49, y: 50, name: 'Green Wood' },
		{ id: 7,	x: 49, y: 51, name: 'Lizard Lowland' },
		{ id: 26,	x: 49, y: 52, name: 'The Wilderness ' },

		{ id: 6,	x: 50, y: 48, name: 'Peaceful Camp' },
		{ id: 3,	x: 50, y: 49, name: 'Tiger Lake' },
		{ id: 1,	x: 50, y: 50, name: 'Empire Capital' },
		{ id: 5,	x: 50, y: 51, name: 'Wolf Dale' },
		{ id: 10,	x: 50, y: 52, name: 'Portal Ruins' },

		{ id: 24,	x: 51, y: 48, name: 'Crystal Garden' },
		{ id: 4,	x: 51, y: 49, name: 'Rogues\' Wood' },
		{ id: 2,	x: 51, y: 50, name: 'East River' },
		{ id: 11,	x: 51, y: 51, name: 'Dragons\' Caves' },
		{ id: 19,	x: 51, y: 52, name: 'Great Wall' },
		{ id: 20,	x: 51, y: 53, name: 'Titans\' Valley' },

		{ id: 16,	x: 52, y: 48, name: 'Fairy Trees' },
		{ id: 15,	x: 52, y: 49, name: 'Bear Mountain' },
		{ id: 14,	x: 52, y: 50, name: 'Magma Mines' },
		{ id: 21,	x: 52, y: 53, name: 'Fishing Village' },
		{ id: 22,	x: 52, y: 54, name: 'Kingdom Castle' },

		{ id: 18,	x: 53, y: 49, name: 'Mithril Coast' },
		{ id: 17,	x: 53, y: 50, name: 'Harbour City' },
	],

	/**
	 * Not linked sectors. Its deny to move between this sectors directly
	 */
	sectorForbiddenLinks: [ [24, 16] ],

	/**
	 * Sectors with mercenary guild
	 */
	sectorMercenary: [ 2, 6, 16, 21 ],

	/**
	 * Calculate all links between sectors
	 * @return Array of arrays, each record have format = [ id1, id2, length ]
	 */
	sectorAllLinks(){
		let links = Array();
		for( let i = 0; i < this.sectorList.length; i++ ){
			for( let j = i+1; j < this.sectorList.length; j++ ){
				// skip forbidden links
				if( this.sectorForbiddenLinks.some( function(s){
						if( s.includes(this.sectorList[i].id) && s.includes(this.sectorList[j].id) ){
							return true;
						}
						return false;
					}, this)
				){
					continue;
				}

				// create link
				if( Math.abs(this.sectorList[i].x - this.sectorList[j].x) <= 1 && Math.abs(this.sectorList[i].y - this.sectorList[j].y) <= 1 ){
					links.push([this.sectorList[i].id, this.sectorList[j].id, Math.abs(this.sectorList[i].x - this.sectorList[j].x) + Math.abs(this.sectorList[i].y - this.sectorList[j].y) == 1 ? 2 : 3 ]);
				}
			}
		}
		return links;
	},

	/**
	 * Return links to near sector for custom sector
	 * @param id - sector id
	 * @return Array with links, link is pair of 2 sector ids
	 */
	getSectorLinks(id){
		let links = Array();
		this.sectorAllLinks().forEach(function(s){
			if( s[0] === id )
				links.push([s[1], s[2]]);
			if( s[1] === id )
				links.push([s[0], s[2]]);
		});
		return links;
	},

	/**
	 * Get sector id from coordinats
	 * @param x sector x-axis
	 * @param y sector y-axis
	 * @return sector id or null if sector not found
	 */
	getSectorIDFromXY(x,y){
		for( let i = 0; i < this.sectorList.length; i++ ){
			if( this.sectorList[i].x == x && this.sectorList[i].y == y ){
				return this.sectorList[i].id;
			}
		}
		return null;
	},

	/**
	 * Get sector name from sector id
	 * @param id sector id
	 * @return sector name or null if sector not found
	 */
	getSectorName(id){
		for( let i = 0; i < this.sectorList.length; i++ ){
			if( this.sectorList[i].id == id ){
				return this.sectorList[i].name;
			}
		}
		return null;
	},

	/**
	 * Return travel map for custom sector
	 * @param[in] id sector id
	 * @return Array with travel map( Array[sectorID] = path length )
	 */
	getSectorMap(id){
		// cant use Array.prototype.length here
		let n = 0;
		this.sectorList.forEach(function(s){
			if( n < s.id ){
				n = s.id + 1;
			}
		});
		// our recursive function for map calculation
		let map = Array(n); map.fill(0); map[id] = -1;
		let calculateMap = function(id){
			this.getSectorLinks(id).forEach(function(s){
				if( map[s[0]] == 0 || map[s[0]] > map[id] + s[1] ){
					map[s[0]] = (map[id] == -1 ? s[1] : map[id] + s[1]);
					calculateMap(s[0]);
				}
			},this);
		}.bind(this);

		calculateMap(id);
		return map;
	},

	/**
	 * Calculate path to custom sector
	 * @param sFrom begin sector
	 * @param sTo end sector
	 * @return Array this sector's ids
	 */
	getPath(sFrom, sTo){
		let path = Array();
		let sMap = this.getSectorMap(sFrom);
		// NOTE Use Array.prototype.some for stop search when we found one
		let calculatePath = function(sID){
			this.getSectorLinks(sID).some( function(s){
				if( sMap[s[0]] < sMap[sID] && ( sMap[sID] - sMap[s[0]] === s[1] || sMap[s[0]] === -1 ) ){
					calculatePath(s[0]);
					path.push(sID);
					return true;
				}
				return false;
			}, this);
			
		}.bind(this);

		calculatePath(sTo);
		return path;
	},
}

/**
 * Class for display notification.
 * Prefer Notification from Native JS, then Greasmonkey notification, fallback to alert()
 */
class Notify {
	/**
	 * Basic constructor for notification
	 * Do not send notification!
	 * @param title Notification title
	 * @param icon Notification icon
	 */
	constructor(title, icon){
		if( "Notification" in window ){
			Notification.requestPermission();
		}
		this.icon = icon;
		this.title = title;
	}

	/**
	 * Create desktop notification
	 * @param data object with message information
	 */
	send(data = {}){
		if( "Notification" in window && Notification.permission === "granted" ){
			let message = new Notification(this.title, {
				body: data.hasOwnProperty('body') ? data.body : '',
				icon: data.hasOwnProperty('icon') ? data.icon : this.icon,
				tag: data.hasOwnProperty('tag') ? data.tag : ''
			} );
			message.onclick = data.hasOwnProperty('onclick') ? data.onclick.bind(message) : function(){ this.close(); };
			setTimeout( message.close.bind(message), data.hasOwnProperty('timeout') ? data.timeout : 10000 );
			window.addEventListener('unload', function(event){ message.close() });
		} else if( typeof GM_notification === 'function' ){
			GM_notification({
				text: data.hasOwnProperty('body') ? data.body : '',
				title: this.title,
				image: data.hasOwnProperty('icon') ? data.icon : this.icon,
				onclick: data.hasOwnProperty('onclick') ? data.onclick : function(){ this.close(); },
				// NOTE Tampermonkey only stuff
				highlight: true,
				timeout: data.hasOwnProperty('timeout') ? data.timeout : 10000,
			});
		} else {
			log.error("Cant notify user, check settings");
		}
	}
};

/**
 * Class for save/load settings in GreasMonkey
 */
class GMSettings {
	/**
	 * Default contructor
	 * @param data object with script options
	 */
	constructor(data = {}){
		this.data = data;
		this.load();
		// create for every option save function
		Object.keys(this.data).forEach(function(key){
			this.data[key].save = function(){
				GM_setValue(key, JSON.stringify(this))
			};
		}, this);
		this.init();
	}

	/**
	 * Load settings from GreasMonkey database
	 */
	load(){
		let savedOpts = GM_listValues();

		savedOpts.forEach(function(key){
			if( this.data[key] ){
				let savedObject = JSON.parse(GM_getValue(key));
				if( Object.keys(this.data[key]).toString() === Object.keys(savedObject).toString() ){
					this.data[key] = savedObject;
				} else {
					GM_setValue(key, JSON.stringify(this.data[key]));
				}
			} else {
				GM_deleteValue(key);
			}
		}, this);
	}

	/**
	 * Check options, save unsaved
	 */
	init(){
		let savedOpts = GM_listValues();

		Object.keys(this.data).forEach(function(key){
			if( !savedOpts.includes(key) ){
				this.data[key].save();
			}
		}, this);
	}
}

/**
 * Script settings tab for all scripts
 * Allow store setting in 1 place, if script have it
 */
const SettingsTab = {
	styleId: 'figey_hwm_style',
	mainTabId: 'figey_hwm_settings',
	overlayId: 'figey_hwm_overlay',
	containerId: 'figey_hwm_container',
	tabListId: 'figey_hwm_tabs',

	/**
	 * Create main tab for settings
	 */
	init(){
		// check style
		let style = document.getElementById(this.styleId);
		if( style === null || style.tagName.toLowerCase() != 'style' ){
			let styleElement = addStyle('#' + this.tabListId + '{list-style-type:none;padding:0;margin:0;position:relative}' +
				'#' + this.tabListId + ':after{content:"";clear:both;display:block;height:240px}' +
				'#' + this.tabListId + ' li{float:left;background-color:#ddd9cd}' +
				'#' + this.tabListId + ' li>input{display:none}' +
				'#' + this.tabListId + ' li>label{display:inline-block;border:1px solid #999;border-right-width:0;border-bottom-width:0;height:30px;line-height:30px;padding:5px 20px;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}' +
				'#' + this.tabListId + ' li:last-child>label{border-right-width:1px}' +
				'#' + this.tabListId + ' .figey_hwm_tab_content{display:none;position:absolute;left:0;padding:3%;border:1px solid #999;height:200px;width:94%;overflow-y:auto}' +
				'#' + this.tabListId + ' li>input:checked+label{background-color:#fff}' +
				'#' + this.tabListId + ' li>input:checked~.figey_hwm_tab_content{display:block}' +
				'#' + this.overlayId + '{position:absolute;top:0;left:0;background:#000;width:100%;opacity:0.7}' +
				'#' + this.containerId + '{position:absolute;top:50px;left:0;right:0;background:#fff;margin:0 auto;padding:10px;border:4px double #000;width:600px;z-index:100;}');
			styleElement.id = this.styleId;
		}

		// check settings element
		let settings = document.getElementById(this.mainTabId);
		if( settings === null ){
			let settingsElement = document.createElement('div');
			settingsElement.id = this.mainTabId;
			settingsElement.style.display = 'none';
			settingsElement.style.zIndex = "1000";
			settingsElement.innerHTML = '<div id="' + this.overlayId + '"></div><div id="' + this.containerId + '"><ul id="' + this.tabListId + '"></ul></div>';
			document.body.insertBefore(settingsElement,document.body.firstChild);

			// add event to hide settings window
			document.getElementById(this.overlayId).addEventListener("click", function(){
				let settingsElement = document.getElementById(this.mainTabId);
				if( settingsElement )
					settingsElement.style.display = 'none';
				else
					log.error('Cant find main tab on the page');
			}.bind(this) );

			document.getElementById(this.overlayId).style.height = Math.max(document.documentElement.scrollHeight,document.body.scrollHeight) + 'px';
			window.addEventListener('resize', function(){ document.getElementById(this.overlayId).style.height = Math.max(document.documentElement.scrollHeight,document.body.scrollHeight) + 'px'; }.bind(this));
		}

		// add event to show settings window
		let entryPoint = document.querySelector("img[src*='gem.gif']");
		if( entryPoint && entryPoint.onclick === null ){
			entryPoint.onclick = function(){
				let settingsElement = document.getElementById(this.mainTabId);
				if( settingsElement )
					settingsElement.style.display = '';
				else
					log.error('Cant find main tab on the page');
			}.bind(this);
		} else if( entryPoint && typeof entryPoint.onclick === 'function' ){
			log.info("Entry point for settings tab already created");
		} else {
			log.error("Cannot create entry point for settings tab");
		}
	},

	/**
	 * Add tab to settings
	 * @param id id for node element
	 * @param name tab name
	 * @return Tab content element for created tab or null if have error
	 */
	add(id, name){
		let tabs = document.getElementById(this.tabListId);
		if( tabs === null ){
			// TODO need to init one more time?
			log.error('Failed create settings tab "' + name + '" for script "' + GM_info.script.name + '"');
			return null;
		}

		// only 1 tab allowed
		let tabId = 'figey_script_' + id;
		if( document.getElementById(tabId) ){
			log.error('Tab with id "' + id + '" already created');
			return null;
		}

		let input = document.createElement('input');
		input.type = 'radio';
		input.checked = !tabs.hasChildNodes();
		input.name = 'tabs';
		input.id = tabId;

		let label = document.createElement('label');
		label.htmlFor = input.id;
		label.textContent = name;

		let tabContent = document.createElement('div');
		tabContent.className = 'figey_hwm_tab_content';

		// NOTE tab content will be added later

		let tab = document.createElement('li');
		tab.appendChild( input );
		tab.appendChild( label );
		tab.appendChild( tabContent );
		tabs.appendChild( tab );

		return tabContent;
	},

};
Object.freeze(SettingsTab);

/**
 * Find parent element by tag name
 * @param node base element
 * @param tag parent element type
 * @param count how many parents we must skip
 * @return parent for custom node or null if no parent
 */
function getParentByTagName(node, tag, count) {
	if( typeof node !== 'object' || node.tagName == 'HTML'
		|| typeof tag !== 'string' || tag == ''
		|| typeof count !== 'number' || count <= 0 
	){
		log.error('Invalid call function ' + this.name);
		return null;
	}

	let parent = node.parentNode;
	tag = tag.toUpperCase();

	while (parent.tagName !== "HTML") {
		if (parent.tagName === tag) {
			if( count == 1 ){
				return parent;
			} else {
				count--;
			}
		}
		parent = parent.parentNode;
	}

	if( parent.tagName !== tag ){
		log.error("Failed found parent for:");
		log.error(node);
		return null;
	}

	return parent;
}
getParentByTagName = getParentByTagName.bind(getParentByTagName);

/**
 * Convert date to HH:MM:SS view
 * @param date datetime
 * @return time in HH:MM:SS format
 */
function extractTime(date){
	if( date.getUTCHours() > 0 ){
		return [date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()].map(function(num){return ('0'+num).slice(-2);}).join(':');
	} else {
		return [date.getUTCMinutes(), date.getUTCSeconds()].map(function(num){return ('0'+num).slice(-2);}).join(':');
	}
}

/**
 * Generate XHR
 * @param method request method(get, put, etc)
 * @param url request url
 * @param sync async request or not
 * @param body request body
 * @return XMLHttpRequest object
 */
function runXHR(method, url, sync, body = null){
	const xhr = new XMLHttpRequest();
	xhr.open(method, url, sync);
	xhr.overrideMimeType('text/html; charset=windows-1251');
	xhr.send(body);
	return xhr;
}

/**
 * Add style to head element
 * @param text css text
 * @return style element
 */
function addStyle(text){
	var head = document.getElementsByTagName("head")[0];
	var oStyle = document.createElement("style");
	oStyle.setAttribute("type", "text/css");
	oStyle.appendChild(document.createTextNode(text));
	head.appendChild(oStyle);
	return oStyle;
}

/**
 * Get mercenary quest status
 * @param text page html
 * @return object with quest status
 */
function mercenaryQuestStatus(text){
	const result = { status:'unknown', time: 0 };

	if( typeof text == 'undefined' || text == null )
		return result;

	// check player status
	const inGuild = text.indexOf('<b>Статус</b>: <font color="red"><b>Вы находитесь в другом районе.</b></font>') >= 0 ? false : true;
	const inBattle = text.indexOf('<b>Статус</b>: <font color="red"><b>Вы находитесь в заявке на бой</b></font>') >= 0 ? true : false;

		// check quest status
	const newQuest = ( text.indexOf('mercenary_guild.php?action=accept') >= 0 || text.indexOf('Для Вас есть задание') >= 0 ) ? true : false;
	const oldQuest = text.match(/сталось (\d+) минут/) || text.match(/\d+ попыток и (\d+) минут</) || text.match(/еще есть (\d+) минут/) || text.match(/(\d+) минут/);

	if( newQuest ){
		result.status = 'new';
		result.time = 0;
	}

	if( oldQuest ){
		result.status = 'old';
		result.time = oldQuest[1];
	}

	if( inGuild && !inBattle ){
		const map = ls.get('map');

		const waitTime = text.match(/Приходи через (\d+) мин/);
		const winQuest = text.match(/Статус<\/b>: <font color="?blue"?>/) ? true : false;
		const failQuest = text.match(/Статус<\/b>: <font color="?red"?>/) ? true : false;

		// success
		if( winQuest ){
			result.status = 'done';
			result.time = waitTime[1];
			result.reward = text.match(/<b>(Вы получаете .*?)<\/b>/)[1];
		// fail
		} else if( failQuest ){
			result.status = 'fail';
			result.time = waitTime[1];
		// correct timer if needed
		} else if( waitTime ){
			result.status = 'wait';
			result.time = waitTime[1];
		}
	}

	return result;
}
